Skip to content

fix: take lock in PriorityJobQueue.is_done - #101

Open
bradjin8 wants to merge 2 commits into
developfrom
fix/queue-is-done-lock
Open

fix: take lock in PriorityJobQueue.is_done#101
bradjin8 wants to merge 2 commits into
developfrom
fix/queue-is-done-lock

Conversation

@bradjin8

@bradjin8 bradjin8 commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Closes #100

Summary

  • PriorityJobQueue.is_done now acquires self._lock when reading _jobs and _completed_keys, matching is_empty and other accessors
  • Adds test_concurrent_is_done_invariant to stress concurrent enqueue/complete while a monitor thread checks that is_done is only True when every job is terminal
  • Changelog entry under [Unreleased] -> ### Fixed

Test plan

  • pytest tests/test_queue.py::TestThreadSafety -v
  • pytest -m "not integration" (full unit suite)
  • pre-commit run -a

Notes

completed_count, running_count, and total_jobs remain unlocked; left for a follow-up per issue scope.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed the job queue completion check to be consistent under concurrent enqueue/complete activity, preventing race conditions that could report incorrect completion status.
  • Tests

    • Strengthened multithreaded coverage to validate that completion status remains accurate while jobs are queued, dequeued, and marked complete concurrently.
  • Documentation

    • Updated the unreleased changelog with an entry describing the queue completion reliability fix.

@bradjin8
bradjin8 requested a review from wpak-ai as a code owner July 30, 2026 20:27
@bradjin8 bradjin8 self-assigned this Jul 30, 2026
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f8d8e306-7aef-4f9c-ad73-a9c3b4921749

📥 Commits

Reviewing files that changed from the base of the PR and between 737c2b5 and bc57070.

📒 Files selected for processing (1)
  • cli/tests/test_queue.py

📝 Walkthrough

Walkthrough

PriorityJobQueue.is_done now evaluates completion state while holding the queue lock. Thread-safety coverage monitors the predicate during concurrent job production and completion, and the changelog documents the fix.

Changes

Queue completion synchronization

Layer / File(s) Summary
Lock completion-state reads
cli/localci/core/queue.py, CHANGELOG.md
PriorityJobQueue.is_done reads queued and completed job counts under self._lock, with the change recorded in the unreleased changelog.
Validate concurrent completion invariants
cli/tests/test_queue.py
Thread-safety tests define terminal statuses and monitor is_done during concurrent enqueue, dequeue, and completion operations.

Estimated code review effort: 2 (Simple) | ~15 minutes

Suggested reviewers: wpak-ai, clean6378-max-it

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: locking PriorityJobQueue.is_done.
Linked Issues check ✅ Passed The lock fix and concurrent stress test address issue #100's coding requirements, and the checked dispatch predicate is unchanged.
Out of Scope Changes check ✅ Passed The changelog note and thread-safety test are in scope; no unrelated code changes are evident.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/queue-is-done-lock

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cli/tests/test_queue.py`:
- Around line 367-375: Fix the TOCTOU race in the monitor function by
re-checking queue.is_done after obtaining the get_all_jobs() snapshot, and only
recording “is_done True with non-terminal jobs” when is_done remains true.
Preserve the existing empty-queue check and terminal-status validation, while
avoiding false violations caused by jobs enqueued between the two calls.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 303a4c71-68e0-4ab8-a538-c5f61b12d85f

📥 Commits

Reviewing files that changed from the base of the PR and between a9aa45a and 737c2b5.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • cli/localci/core/queue.py
  • cli/tests/test_queue.py

Comment thread cli/tests/test_queue.py
@clean6378-max-it

Copy link
Copy Markdown
Collaborator

queue.py:233 - move WAITING_DEPS jobs back to QUEUED once their dependencies finish, the way line 339 already does for WAITING_PRIORITY (nothing ever moves a job out of WAITING_DEPS. Two jobs linked by needs land at the same priority by default, so the dependent is stranded, the level never completes, and _dispatch_loop spins forever) pre-existing

Comment thread cli/tests/test_queue.py
t.join()
assert queue.total_jobs == 100

def test_concurrent_is_done_invariant(self):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_queue.py:356, CHANGELOG.md:28 - the test is not a regression guard.
Give it a failure mode or rename it, and reword the changelog as a consistency fix rather than a fixed race
(with the lock reverted it passed 20 out of 20 runs across 699 observations of is_done == True.
The unlocked read can only go stale in the harmless direction, so a future revert goes unnoticed)

Comment thread cli/tests/test_queue.py
with violations_lock:
violations.append(msg)

def monitor() -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

read is_done and the job statuses in one locked call, instead of calling is_done and then inspecting what get_all_jobs returned
(get_all_jobs hands back live job objects, so the monitor checks status after the lock is gone and any transient has already healed. Only worth doing if the test can fail at all, since it means adding a production accessor for one test)

Comment thread cli/tests/test_queue.py
stop.set()
monitor_thread.join()

assert not violations, violations

@clean6378-max-it clean6378-max-it Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assert total_jobs == 100 and passed_count == 100, and have the monitor count it's samples with an assertion
That the count is above zero

(neither assertion pins the job count or shows thee monitor ran , and assert queue.js_done is Trus just restates the consumer loop's own exit condition, so a dead producer leaves the test green)

Comment thread cli/tests/test_queue.py
queue.enqueue(make_job(f"Job {i}", priority=1, index=i))
time.sleep(0.001)

def consume() -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

put a deadline on the consumer loop, give every join a timeout, and set timeout-minutes on the ci.yml test job
(the loop exits only when is_done goes true and the joins are unbounded. pytest-timeout is not installed and only the integration job sets a timeout, so finding 1 burns the 6-hour Actions default on three Python versions instead of failing)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Take the lock in PriorityJobQueue.is_done

2 participants